package de.fmaul.common.io;

import java.io.File;
import java.util.Iterator;
import java.util.List;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;

/**
 * The {@link FileFinder} is used to configure the options for the search. This
 * is done by following the builder pattern. Several methods can be used to
 * influence the search.
 * <p>
 * To define if you want only files, only directories or both in your result you
 * can use the {@link #yieldFiles()}, {@link #yieldDirectories()} or
 * {@link #yieldFilesAndDirectories()} methods.
 * <p>
 * Without any options only the current directory is searched. The
 * {@link #recursive()} and {@link #recursive(Predicate)} methods allow you to
 * enable recursive searching.
 * <p>
 * You can search for specific filter by using {@link #withName(String)},
 * {@link #withExtension(String)} or the custom {@link #withFilter(Predicate)}
 * methods. The string comparison of filenames can be fine tuned with the
 * {@link #caseSensitive()} and {@link #ignoreCase()} methods.
 * <p>
 * All these methods can be chained together with the following limitations.
 * <ul>
 * <li>{@link #caseSensitive()} and {@link #ignoreCase()} only affect the
 * following filter definitions.
 * <li>Only one yield* makes sense. If you use multiple the last one wins.
 * </ul>
 * To finally execute the search you have two options.
 * <ul>
 * <li>Use the {@link #list()} method to execute the search in one go and get
 * all the results in a {@link List<File>}.
 * <li>Call {@link #iterator()} and get the results piece by piece. Since the
 * {@link FileFinder} implements {@link Iterable} it can even be used in a for
 * each loop.
 * </ul>
 * <p>
 * Examples:
 * <code><pre>
 * // Iterate over all files in the windows directory
 * for (File f : Files.find("c:\\windows")) { ... }
 * 
 * // Get all the files in a directory as a list of files.
 * List<File> allFiles = Files.find(somedir).list();
 * 
 * // Skip all .svn directories within a source tree
 * Predicate<File> noSvnDirs = new Predicate<File>() {
 *   boolean apply(File file) {
 *     return !file.getName().equals(".svn");
 *   }
 * }
 * for (File f : Files.find("src/java/").recursive(noSvnDir)) { ... }
 * </code></pre>
 * 
 * @author Florian Maul
 */
public class FileFinder implements Iterable<File> {

	/**
	 * Predicate that returns true, when a {@link File}-object points actually
	 * to a file.
	 */
	private final static Predicate<File> isFile = new Predicate<File>() {
		public boolean apply(File input) {
			return input.isFile();
		};
	};

	/**
	 * Predicate that returns true, when a {@link File}-object points to a
	 * directory.
	 */
	private final static Predicate<File> isDirectory = new Predicate<File>() {
		public boolean apply(File input) {
			return input.isDirectory();
		};
	};

	/**
	 * The base directory which will be used for the search
	 */
	private final File baseDir;

	/**
	 * A filter which determines which type of files will be returned (files,
	 * directories or both).
	 */
	private Predicate<File> yieldFilter = isFile;

	/**
	 * A {@link Predicate}-filter which determines which will be processed
	 * recursively.
	 */
	private Predicate<File> branchFilter = Predicates.alwaysFalse();

	/**
	 * A filter that can be used to filter specific files.
	 */
	private Predicate<File> fileFilter = Predicates.alwaysTrue();

	/**
	 * A boolean option that that defines if {@link String} comparisons are made
	 * case sensitive or insensitive, e.g. for filenames and extensions. The
	 * default are case sensitive comparisons.
	 */
	private boolean caseSensitive = true;

	/**
	 * Creates a new {@link FileFinder} object for a given base directory.
	 * 
	 * @param baseDir
	 *            The base directory where the search starts.
	 */
	public FileFinder(File baseDir) {
		this.baseDir = baseDir;
	}

	/**
	 * Returns the result of the search as a list.
	 * 
	 * @return A list with the files that where found.
	 */
	public List<File> list() {
		return Lists.newArrayList(iterator());
	}

	/**
	 * Creates an Iterator that can be used to iterate through the results of
	 * the search. Note: This actually works iterative, i.e. the recursion
	 * happens as you fetch files from the iterator. The result is not fetched
	 * into a huge list.
	 * 
	 * @return An {@link Iterator<File>} that retrieves the results bit by bit.
	 * 
	 * @see java.lang.Iterable#iterator()
	 */
	public Iterator<File> iterator() {
		return new FileFinderInterator(baseDir, yieldFilter, branchFilter,
				fileFilter);
	}

	/**
	 * Configures the {@link FileFinder} to return files.
	 * 
	 * @return The current {@link FileFinder} to perform method chaining.
	 */
	public FileFinder yieldFiles() {
		yieldFilter = isFile;
		return this;
	}

	/**
	 * Configures the {@link FileFinder} to return directories.
	 * 
	 * @return The current {@link FileFinder} to perform method chaining.
	 */
	public FileFinder yieldDirectories() {
		yieldFilter = isDirectory;
		return this;
	}

	/**
	 * Configures the {@link FileFinder} to return files and directories.
	 * 
	 * @return The current {@link FileFinder} to perform method chaining.
	 */
	public FileFinder yieldFilesAndDirectories() {
		yieldFilter = Predicates.or(isFile, isDirectory);
		return this;
	}

	/**
	 * Configures the {@link FileFinder} to use case sensitive comparisons for
	 * filenames.
	 * 
	 * @return The current {@link FileFinder} to perform method chaining.
	 */
	public FileFinder caseSensitive() {
		caseSensitive = true;
		return this;
	}

	/**
	 * Configures the {@link FileFinder} to ignore the case for comparisons of
	 * filenames.
	 * 
	 * @return The current {@link FileFinder} to perform method chaining.
	 */
	public FileFinder ignoreCase() {
		caseSensitive = false;
		return this;
	}

	/**
	 * Defines a search for a file with a specific filename. A contains
	 * comparison is used so the filename can be a partial match.
	 * <p>
	 * Note that this method uses the case settings you can set with
	 * {@link #ignoreCase()} and {@link #caseSensitive()}.
	 * 
	 * @param name
	 *            The filename to search for as a {@link String}.
	 * @return The current {@link FileFinder} to perform method chaining.
	 */
	public FileFinder withName(final String name) {
		return withFilter(new FileNameMatchPredicate(name, caseSensitive, true));
	}

	/**
	 * Defines a search for a file extension.
	 * <p>
	 * Note that this method uses the case settings you can set with
	 * {@link #ignoreCase()} and {@link #caseSensitive()}.
	 * 
	 * @param name
	 *            The file extension to search for as a {@link String}.
	 * @return The current {@link FileFinder} to perform method chaining.
	 */
	public FileFinder withExtension(final String extension) {
		return withFilter(new FileExtensionMatchPredicate(extension, caseSensitive));
	}

	/**
	 * Enables a recursive search that processes all sub directories recursively
	 * with a depth-first search.
	 * 
	 * @return The current {@link FileFinder} to perform method chaining.
	 */
	public FileFinder recursive() {
		branchFilter = Predicates.alwaysTrue();
		return this;
	}

	/**
	 * Enables a recursive search that processes sub directories that match the
	 * given {@link Predicate} recursively with a depth-first search.
	 * 
	 * @param branchFilter
	 *            The {@link Predicate<File>} that returns true for all
	 *            directories that should be used in the search.
	 * @return The current {@link FileFinder} to perform method chaining.
	 */
	public FileFinder recursive(Predicate<File> branchFilter) {
		this.branchFilter = branchFilter;
		return this;
	}

	/**
	 * Accepts a custom filter to search for specific files or directories. All
	 * files that match the {@link Predicate} (and match the yield* file type)
	 * will be returned in the result.
	 * <p>
	 * Multiple calls of {@link #withFilter(Predicate)} as well as other filter
	 * methods like {@link #withName(String)} will be combined with an AND
	 * condition, i.e. all filters have to match.
	 * 
	 * @param filter
	 *            The {@link Predicate} that should be used to filter files.
	 * @return The current {@link FileFinder} to perform method chaining.
	 */
	public FileFinder withFilter(Predicate<File> filter) {
		this.fileFilter = Predicates.and(fileFilter, filter);
		return this;
	}

	public FileFinder containingFile(final Predicate<File> fileInDir) {
		return withFilter(new Predicate<File>() {
			public boolean apply(File directory) {
				return directoryContainsFile(directory, fileInDir);
			}
		});
	}

	public FileFinder containingFile(final String filename) {
		return containingFile(new FileNameMatchPredicate(filename, caseSensitive, false));
	}

	public FileFinder contains(String text) {
		return withFilter(new StringContentMatcher(text));
	}

	public FileFinder contains(byte[] bytes) {
		throw new IllegalArgumentException();
	}

	private boolean directoryContainsFile(File directory,
			final Predicate<File> containgFileFilter) {
		if (directory.isDirectory()) {
			File[] allFiles = directory.listFiles();
			for (File file : allFiles) {
				if (containgFileFilter.apply(file)) {
					return true;
				}
			}
		}
		return false;
	}

}
